﻿using Gargoyle.Roton;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Gargoyle.ZZT.File
{
    sealed class ZZTBinaryReader : BinaryReader
    {
        public ZZTBinaryReader(Stream input) : base(input)
        {
        }

        public ByteString ReadPascalString()
        {
            byte length = ReadByte();
            return (ByteString)ReadBytes(length);
        }

        public ByteString ReadPascalString(int fixedLength)
        {
            ByteString result = new byte[fixedLength];
            byte length;

            length = ReadByte();
            result = ReadBytes(fixedLength);
            result.Resize(length);

            return result;
        }
    }

    sealed class ZZTBinaryWriter : BinaryWriter
    {
        public ZZTBinaryWriter(Stream output) : base(output)
        {
        }

        public void WritePascalString(ByteString sourceBytes)
        {
            unchecked
            {
                byte length = (byte)sourceBytes.Length;
                byte[] bytes = sourceBytes;
                Write(length);
                Write(bytes);
            }
        }

        public void WritePascalString(int fixedLength, ByteString sourceBytes)
        {
            ByteString result = new byte[fixedLength];
            byte length = (byte)sourceBytes.Length;

            Array.Copy(sourceBytes, result, sourceBytes.Length);
            Write(length);
            Write((byte[])result);
        }
    }

    abstract class ConverterBase
    {
        protected ZZTBinaryReader reader;
        protected Stream stream;
        protected ZZTBinaryWriter writer;

        public ConverterBase(Stream newStream)
        {
            stream = newStream;
            reader = new ZZTBinaryReader(stream);
            writer = new ZZTBinaryWriter(stream);
        }
    }

    abstract class BoardConverter : ConverterBase
    {
        protected class RLECodec : ConverterBase
        {
            public RLECodec(Stream newStream) : base(newStream)
            {
            }

            public Tile[] Decode(int totalTiles)
            {
                List<Tile> result = new List<Tile>();
                byte element = 0;
                int count = 0;
                byte color = 0;
                int index = 0;

                while (index < totalTiles)
                {
                    if (count == 0)
                    {
                        count = (int)reader.ReadByte();
                        element = reader.ReadByte();
                        color = reader.ReadByte();
                    }
                    count = (count - 1) & 0xFF;
                    result.Add(new Tile(element, color));
                    index++;
                }

                return result.ToArray();
            }

            public void Encode(Tile[] tiles)
            {
                List<byte> result = new List<byte>();
                byte count = 1;
                byte element = tiles[0].Element;
                byte color = tiles[0].Color;
                int index = 1;

                while (index < tiles.Length)
                {
                    if ((count == 255) || (element != tiles[index].Element) || (color != tiles[index].Color))
                    {
                        result.Add(count);
                        result.Add(element);
                        result.Add(color);
                        count = 0;
                        element = tiles[index].Element;
                        color = tiles[index].Color;
                    }
                    count++;
                    index++;
                }
                result.Add(count);
                result.Add(element);
                result.Add(color);
                stream.Write(result.ToArray(), 0, result.Count);
            }
        }

        protected ElementCollection Elements;
        protected Parameters Parameters;

        public BoardConverter(Stream newStream, ElementCollection newElements, Parameters newParameters) : base(newStream)
        {
            Elements = newElements;
            Parameters = newParameters;
        }

        protected int Area
        {
            get
            {
                return (Parameters.BoardWidth * Parameters.BoardHeight);
            }
        }

        protected byte[] GetRawData()
        {
            Int16 length = reader.ReadInt16();
            byte[] rawData = new byte[length];
            stream.Read(rawData, 0, length);
            return rawData;
        }

        abstract protected int Padding { get; }

        public BoardCollection Read(Int16 count, Heap heap)
        {
            BoardCollection result = new BoardCollection(Elements);

            for (int i = 0; i < count; i++)
            {
                result.Add(ReadBoard(heap));
            }

            return result;
        }

        public virtual Board ReadBoard(Heap heap)
        {
            return null;
        }

        protected ThingCollection ReadThings(ThingConverter converter, int capacity, Int16 count, Heap heap)
        {
            ThingCollection result = new ThingCollection(heap, capacity, converter.Padding);
            Thing thing;

            for (int i = 0; i < count; i++)
            {
                thing = converter.Read(heap);
                if (thing.Length < 0)
                {
                    thing.Pointer = result[-thing.Length].Pointer;
                    thing.Length = result[-thing.Length].Length;
                }
                result.Add(thing);
            }

            List<Thing> tempList = result.ToList();
            return result;
        }

        public void Write(BoardCollection collection)
        {
            Int16 count = (Int16)collection.Count;

            for (int i = 0; i < count; i++)
            {
                WriteBoard(collection[i]);
            }
        }

        public virtual void WriteBoard(Board board)
        {
        }

        protected void WriteThings(ThingCollection things, ThingConverter converter)
        {
            Thing thing;
            List<ByteString> codeBank = new List<ByteString>();
            int codeIndex;

            for (int i = 0; i < things.Count; i++)
            {
                codeIndex = codeBank.IndexOf(things[i].Code);
                if (codeIndex >= 0)
                {
                    thing = things[i].Clone();
                    thing.Length = (Int16)(-codeIndex);
                    thing.Code = null;
                    converter.Write(thing);
                }
                else
                {
                    codeBank.Add(things[i].Code);
                    converter.Write(things[i]);
                }
            }
        }
    }

    abstract class InfoConverter : ConverterBase
    {
        protected Parameters Parameters;

        public InfoConverter(Stream newStream, Parameters newParameters)
            : base(newStream)
        {
            Parameters = newParameters;
        }

        public virtual Info Read()
        {
            return null;
        }

        public virtual void Write(Info info)
        {
        }
    }

    abstract class ThingConverter : ConverterBase
    {
        public ThingConverter(Stream newStream) : base(newStream)
        {
        }

        abstract public int Padding { get; }

        public virtual Thing Read(Heap heap)
        {
            Thing result = new Thing(heap);
            ReadProperties(ref result);
            return result;
        }

        protected void ReadProperties(ref Thing thing)
        {
            thing.Location.X = reader.ReadByte();
            thing.Location.Y = reader.ReadByte();
            thing.XStep = reader.ReadInt16();
            thing.YStep = reader.ReadInt16();
            thing.Cycle = reader.ReadInt16();
            thing.P1 = reader.ReadByte();
            thing.P2 = reader.ReadByte();
            thing.P3 = reader.ReadByte();
            thing.Follower = reader.ReadInt16();
            thing.Leader = reader.ReadInt16();
            thing.UnderTile.Element = reader.ReadByte();
            thing.UnderTile.Color = reader.ReadByte();
            reader.ReadInt32();
            thing.Instruction = reader.ReadInt16();
            thing.Length = reader.ReadInt16();
        }

        public virtual void Write(Thing thing)
        {
            WriteProperties(thing);
        }

        protected void WriteProperties(Thing thing)
        {
            writer.Write(thing.Location.X);
            writer.Write(thing.Location.Y);
            writer.Write(thing.XStep);
            writer.Write(thing.YStep);
            writer.Write(thing.Cycle);
            writer.Write(thing.P1);
            writer.Write(thing.P2);
            writer.Write(thing.P3);
            writer.Write(thing.Follower);
            writer.Write(thing.Leader);
            writer.Write(thing.UnderTile.Element);
            writer.Write(thing.UnderTile.Color);
            writer.Write((Int32)0x00000000);
            writer.Write(thing.Instruction);
            writer.Write(thing.Length);
        }
    }

    class WorldConverter : ConverterBase
    {
        protected Parameters Parameters;

        public WorldConverter(Stream newStream, Parameters newParameters)
            : base(newStream)
        {
            Parameters = newParameters;
        }

        private void GetConverters(Int16 type, ElementCollection elements, ref InfoConverter infoConverter, ref BoardConverter boardConverter)
        {
            switch (type)
            {
                case -1:
                    infoConverter = new ZZTInfoConverter(stream, Parameters);
                    boardConverter = new ZZTBoardConverter(stream, elements, Parameters);
                    break;
                case -2:
                    infoConverter = new SuperZZTInfoConverter(stream, Parameters);
                    boardConverter = new SuperZZTBoardConverter(stream, elements, Parameters);
                    break;
                default:
                    infoConverter = null;
                    boardConverter = null;
                    break;
            }
        }

        private void GetEngineInfo(Int16 type, ref ElementCollection elements)
        {
            switch (type)
            {
                case -1:
                    elements = new ElementCollection(ElementList.ZZT());
                    break;
                case -2:
                    elements = new ElementCollection(ElementList.SuperZZT());
                    break;
                default:
                    elements = null;
                    break;
            }
        }

        public World Read(Int16 desiredType, Heap heap)
        {
            Int16 type = reader.ReadInt16();
            Int16 boardCount = (Int16)(reader.ReadInt16() + 1);
            BoardConverter boardConverter = null;
            BoardCollection boards = null;
            ElementCollection elements = null;
            Info info = null;
            InfoConverter infoConverter = null;
            Parameters parameters = null;

            if (desiredType == type)
            {
                GetEngineInfo(type, ref elements);
                GetConverters(type, elements, ref infoConverter, ref boardConverter);
                if (infoConverter != null)
                    info = infoConverter.Read();
                if (boardConverter != null)
                    boards = boardConverter.Read(boardCount, heap);
                return new World(type, info, boards, elements, parameters);
            }

            return null;
        }

        public void Write(World game)
        {
            InfoConverter infoConverter = null;
            BoardConverter boardConverter = null;

            GetConverters(game.Type, game.Elements, ref infoConverter, ref boardConverter);
            writer.Write(game.Type);
            writer.Write((Int16)(game.Boards.Count - 1));
            if (infoConverter != null)
                infoConverter.Write(game.Info);
            if (boardConverter != null)
                boardConverter.Write(game.Boards);
        }
    }

    class ZZTBoardConverter : BoardConverter
    {
        private const int MessageLength = 58;
        private const int PaddingBytes = 16;

        public ZZTBoardConverter(Stream newStream, ElementCollection newElements, Parameters newParameters)
            : base(newStream, newElements, newParameters)
        {
        }

        protected override int Padding
        {
            get { return PaddingBytes; }
        }

        public override Board ReadBoard(Heap heap)
        {
            MemoryStream rawDataStream = new MemoryStream(GetRawData());
            ZZTBinaryReader reader = new ZZTBinaryReader(rawDataStream);
            Board result = new Board(Parameters.BoardWidth, Parameters.BoardHeight);
            ThingConverter converter = new ZZTThingConverter(rawDataStream);
            RLECodec codec = new RLECodec(rawDataStream);

            try
            {
                result.Name = reader.ReadPascalString(Parameters.BoardNameLength);
                Array.Copy(codec.Decode(Area), result.Tiles, Area);
                result.Shots = reader.ReadByte();
                result.Dark = reader.ReadByte();
                result.ExitNorth = reader.ReadByte();
                result.ExitSouth = reader.ReadByte();
                result.ExitWest = reader.ReadByte();
                result.ExitEast = reader.ReadByte();
                result.RestartOnZap = reader.ReadByte();
                reader.ReadBytes(MessageLength + 1);
                result.Enter = new Location();
                result.Enter.X = reader.ReadByte();
                result.Enter.Y = reader.ReadByte();
                result.TimeLimit = reader.ReadInt16();
                reader.ReadBytes(PaddingBytes);
                result.Things = ReadThings(converter, Parameters.MaxThings, (Int16)(reader.ReadInt16() + 1), heap);
            }
            catch
            {
            }
            return result;
        }

        public override void WriteBoard(Board board)
        {
            MemoryStream rawDataStream = new MemoryStream();
            ZZTBinaryWriter writer = new ZZTBinaryWriter(rawDataStream);
            ThingConverter converter = new ZZTThingConverter(rawDataStream);
            RLECodec codec = new RLECodec(rawDataStream);

            writer.WritePascalString(Parameters.BoardNameLength, board.Name);
            codec.Encode(board.Tiles);
            writer.Write(board.Shots);
            writer.Write(board.Dark);
            writer.Write(board.ExitNorth);
            writer.Write(board.ExitSouth);
            writer.Write(board.ExitWest);
            writer.Write(board.ExitEast);
            writer.Write(board.RestartOnZap);
            writer.Write(new byte[MessageLength + 1]);
            writer.Write(board.Enter.X);
            writer.Write(board.Enter.Y);
            writer.Write(board.TimeLimit);
            writer.Write(new byte[PaddingBytes]);
            writer.Write((Int16)(board.Things.Count - 1));
            WriteThings(board.Things, converter);
            writer.Flush();
            base.writer.Write((Int16)rawDataStream.Length);
            base.writer.Write(rawDataStream.ToArray());
        }
    }

    class SuperZZTBoardConverter : BoardConverter
    {
        private const int PaddingBytes = 14;

        public SuperZZTBoardConverter(Stream newStream, ElementCollection newElements, Parameters newParameters)
            : base(newStream, newElements, newParameters)
        {
        }

        protected override int Padding
        {
            get { return PaddingBytes; }
        }

        public override Board ReadBoard(Heap heap)
        {
            MemoryStream rawDataStream = new MemoryStream(GetRawData());
            ZZTBinaryReader reader = new ZZTBinaryReader(rawDataStream);
            Board result = new Board(Parameters.BoardWidth, Parameters.BoardHeight);
            ThingConverter converter = new SuperZZTThingConverter(rawDataStream);
            RLECodec codec = new RLECodec(rawDataStream);

            try
            {
                result.Name = reader.ReadPascalString(Parameters.BoardNameLength);
                Array.Copy(codec.Decode(Area), result.Tiles, Area);
                result.Shots = reader.ReadByte();
                result.ExitNorth = reader.ReadByte();
                result.ExitSouth = reader.ReadByte();
                result.ExitWest = reader.ReadByte();
                result.ExitEast = reader.ReadByte();
                result.RestartOnZap = reader.ReadByte();
                result.Enter = new Location();
                result.Enter.X = reader.ReadByte();
                result.Enter.Y = reader.ReadByte();
                reader.ReadInt16();
                reader.ReadInt16();
                result.TimeLimit = reader.ReadInt16();
                reader.ReadBytes(PaddingBytes);
                result.Things = ReadThings(converter, Parameters.MaxThings, (Int16)(reader.ReadInt16() + 1), heap);
            }
            catch
            {
            }
            return result;
        }

        public override void WriteBoard(Board board)
        {
            MemoryStream rawDataStream = new MemoryStream();
            ZZTBinaryWriter writer = new ZZTBinaryWriter(rawDataStream);
            ThingConverter converter = new SuperZZTThingConverter(rawDataStream);
            RLECodec codec = new RLECodec(rawDataStream);

            writer.WritePascalString(Parameters.BoardNameLength, board.Name);
            codec.Encode(board.Tiles);
            writer.Write(board.Shots);
            writer.Write(board.ExitNorth);
            writer.Write(board.ExitSouth);
            writer.Write(board.ExitWest);
            writer.Write(board.ExitEast);
            writer.Write(board.RestartOnZap);
            writer.Write(board.Enter.X);
            writer.Write(board.Enter.Y);
            writer.Write((Int16)0x0000);
            writer.Write((Int16)0x0000);
            writer.Write(board.TimeLimit);
            writer.Write(new byte[PaddingBytes]);
            WriteThings(board.Things, converter);
            writer.Flush();
            base.writer.Write((Int16)rawDataStream.Length);
            base.writer.Write(rawDataStream.ToArray());
        }
    }

    class ZZTInfoConverter : InfoConverter
    {
        private const int HeaderLength = 0x200 - 4;

        public ZZTInfoConverter(Stream newStream, Parameters newParameters)
            : base(newStream, newParameters)
        {
        }

        public override Info Read()
        {
            ZZTBinaryReader reader = new ZZTBinaryReader(new MemoryStream(base.reader.ReadBytes(HeaderLength)));
            Info result = new Info();

            result.Ammo = reader.ReadInt16();
            result.Gems = reader.ReadInt16();
            result.Keys = reader.ReadBytes(Parameters.KeyCount);
            result.Health = reader.ReadInt16();
            result.Board = reader.ReadInt16();
            result.Torches = reader.ReadInt16();
            result.TorchCycles = reader.ReadInt16();
            result.EnergyCycles = reader.ReadInt16();
            reader.ReadInt16();
            result.Score = reader.ReadInt16();
            result.Name = reader.ReadPascalString(Parameters.WorldNameLength);
            result.Flags = new ByteString[Parameters.MaxFlags];
            for (int i = 0; i < Parameters.MaxFlags; i++)
            {
                result.Flags[i] = reader.ReadPascalString(Parameters.FlagNameLength);
            }
            result.TimePassed = reader.ReadInt16();
            reader.ReadInt16();
            result.SavedGame = reader.ReadByte();

            return result;
        }

        public override void Write(Info info)
        {
            byte[] rawData = new byte[HeaderLength];
            ZZTBinaryWriter writer = new ZZTBinaryWriter(new MemoryStream(rawData));

            writer.Write(info.Ammo);
            writer.Write(info.Gems);
            writer.Write(info.Keys);
            writer.Write(info.Health);
            writer.Write(info.Board);
            writer.Write(info.Torches);
            writer.Write(info.TorchCycles);
            writer.Write(info.EnergyCycles);
            writer.Write((Int16)0x0000);
            writer.Write(info.Score);
            writer.WritePascalString(Parameters.WorldNameLength, info.Name);
            for (int i = 0; i < Parameters.MaxFlags; i++)
            {
                writer.WritePascalString(Parameters.FlagNameLength, info.Flags[i]);
            }
            writer.Write(info.TimePassed);
            writer.Write((Int16)0x0000);
            writer.Write(info.SavedGame);

            writer.Flush();
            stream.Write(rawData, 0, HeaderLength);
        }
    }

    class SuperZZTInfoConverter : InfoConverter
    {
        private const int HeaderLength = 0x400 - 4;

        public SuperZZTInfoConverter(Stream newStream, Parameters newParameters)
            : base(newStream, newParameters)
        {
        }

        public override Info Read()
        {
            ZZTBinaryReader reader = new ZZTBinaryReader(new MemoryStream(base.reader.ReadBytes(HeaderLength)));
            Info result = new Info();

            result.Ammo = reader.ReadInt16();
            result.Gems = reader.ReadInt16();
            result.Keys = reader.ReadBytes(Parameters.KeyCount);
            result.Health = reader.ReadInt16();
            result.Board = reader.ReadInt16();
            reader.ReadInt16();
            result.Score = reader.ReadInt16();
            reader.ReadInt16();
            result.EnergyCycles = reader.ReadInt16();
            result.Name = reader.ReadPascalString(Parameters.WorldNameLength);
            result.Flags = new ByteString[Parameters.MaxFlags];
            for (int i = 0; i < Parameters.MaxFlags; i++)
            {
                result.Flags[i] = reader.ReadPascalString(Parameters.FlagNameLength);
            }
            result.TimePassed = reader.ReadInt16();
            reader.ReadInt16();
            result.SavedGame = reader.ReadByte();
            result.Stones = reader.ReadInt16();

            return result;
        }

        public override void Write(Info info)
        {
            byte[] rawData = new byte[HeaderLength];
            ZZTBinaryWriter writer = new ZZTBinaryWriter(new MemoryStream(rawData));

            writer.Write(info.Ammo);
            writer.Write(info.Gems);
            writer.Write(info.Keys);
            writer.Write(info.Health);
            writer.Write(info.Board);
            writer.Write((Int16)0x0000);
            writer.Write(info.Score);
            writer.Write((Int16)0x0000);
            writer.Write(info.EnergyCycles);
            writer.WritePascalString(Parameters.WorldNameLength, info.Name);
            for (int i = 0; i < Parameters.MaxFlags; i++)
            {
                writer.WritePascalString(Parameters.FlagNameLength, info.Flags[i]);
            }
            writer.Write(info.TimePassed);
            writer.Write((Int16)0x0000);
            writer.Write(info.SavedGame);
            writer.Write(info.Stones);

            writer.Flush();
            stream.Write(rawData, 0, HeaderLength);
        }
    }

    class ZZTThingConverter : ThingConverter
    {
        private const int PaddingBytes = 8;

        public ZZTThingConverter(Stream newStream)
            : base(newStream)
        {
        }

        public override int Padding
        {
            get { return PaddingBytes; }
        }

        public override Thing Read(Heap heap)
        {
            Thing result = ReadBase(heap);
            ReadPadding();
            ReadCode(result);
            return result;
        }

        protected Thing ReadBase(Heap heap)
        {
            Thing result = new Thing(heap);

            result.Location.X = reader.ReadByte();
            result.Location.Y = reader.ReadByte();
            result.XStep = reader.ReadInt16();
            result.YStep = reader.ReadInt16();
            result.Cycle = reader.ReadInt16();
            result.P1 = reader.ReadByte();
            result.P2 = reader.ReadByte();
            result.P3 = reader.ReadByte();
            result.Follower = reader.ReadInt16();
            result.Leader = reader.ReadInt16();
            result.UnderTile.Element = reader.ReadByte();
            result.UnderTile.Color = reader.ReadByte();
            reader.ReadInt32();
            result.Instruction = reader.ReadInt16();
            result.Length = reader.ReadInt16();

            return result;
        }

        protected void ReadCode(Thing thing)
        {
            if (thing.Length > 0)
                thing.Code = reader.ReadBytes(thing.Length);
        }

        protected void ReadPadding()
        {
            reader.ReadBytes(PaddingBytes);
        }

        public override void Write(Thing thing)
        {
            WriteBase(thing);
            WritePadding();
            WriteCode(thing);
        }

        protected void WriteBase(Thing thing)
        {
            writer.Write(thing.Location.X);
            writer.Write(thing.Location.Y);
            writer.Write(thing.XStep);
            writer.Write(thing.YStep);
            writer.Write(thing.Cycle);
            writer.Write(thing.P1);
            writer.Write(thing.P2);
            writer.Write(thing.P3);
            writer.Write(thing.Follower);
            writer.Write(thing.Leader);
            writer.Write(thing.UnderTile.Element);
            writer.Write(thing.UnderTile.Color);
            writer.Write((Int32)0x00000000);
            writer.Write(thing.Instruction);
            writer.Write(thing.Length);
        }

        protected void WriteCode(Thing thing)
        {
            if (thing.Length > 0)
                writer.Write((byte[])thing.Code);
        }

        protected void WritePadding()
        {
            writer.Write(new byte[PaddingBytes]);
        }
    }

    class SuperZZTThingConverter : ZZTThingConverter
    {
        public SuperZZTThingConverter(Stream newStream)
            : base(newStream)
        {
        }

        public override int Padding
        {
            get
            {
                return 0;
            }
        }

        public override Thing Read(Heap heap)
        {
            Thing result = ReadBase(heap);
            ReadCode(result);
            return result;
        }

        public override void Write(Thing thing)
        {
            WriteBase(thing);
            WriteCode(thing);
        }
    }

    internal class DataCache
    {
        static public Cache Read(byte[] source)
        {
            Stream sourceStream = new MemoryStream(source);
            ZZTBinaryReader reader = new ZZTBinaryReader(sourceStream);
            Cache result = new Cache("");
            Int16 count;
            int total = 24;
            ByteString[] fileNames = new ByteString[total];
            Int32[] offsets = new Int32[total];

            count = reader.ReadInt16();
            for (int i = 0; i < total; i++)
            {
                fileNames[i] = reader.ReadPascalString(50);
            }
            for (int i = 0; i < total; i++)
            {
                offsets[i] = reader.ReadInt32();
            }
            for (int i = 0; i < count; i++)
            {
                List<byte> fileData = new List<byte>();
                sourceStream.Position = offsets[i];
                while (true)
                {
                    ByteString line = reader.ReadPascalString();
                    if (line.Length == 1 && line[0] == 0x40)
                    {
                        break;
                    }
                    fileData.AddRange((byte[])(line));
                    fileData.Add(0x0D);
                }
                if (fileData.Count > 0)
                {
                    fileData.RemoveAt(fileData.Count - 1);
                }
                result.SaveFile(fileNames[i], fileData.ToArray(), false);
            }

            reader.Close();
            return result;
        }
    }
}
